我們介紹了很多併發及go的內部原理後,我們終於要來碰實作啦,感謝大家的耐心,這集的篇幅會偏長,請容我細細道來
在開始前,我們要來複習一下上一節的觀念
Go語言使用 go
關鍵字 + 函數/方法創建goroutine
go fmt.Println("create a goroutine")
但只是這樣還沒辦法發揮go的強大之處,CSP 模型強調通過通信來共享記憶體,而不是通過共享記憶體來通信。在 Go 中,這種通信主要是通過 channel 來實現的。channel 是 Go 中的一個核心特性
package main
import (
"fmt"
"time"
)
func worker(msgCh chan string, replyCh chan string) {
// 從 msgCh 接收消息
msg := <-msgCh
fmt.Println("Worker received:", msg)
// 發送一個回應到 replyCh
replyCh <- "Work done!"
}
func main() {
msgCh := make(chan string)
replyCh := make(chan string)
// 啟動 worker goroutine
go worker(msgCh, replyCh)
// 發送一個消息到 worker
msgCh <- "Do some work"
// 接收 worker 的回應
reply := <-replyCh
fmt.Println("Main received:", reply)
// 等待一秒鐘來觀察輸出,否則主函數結束程式就會退出
time.Sleep(1 * time.Second)
}
其實對一個原本是寫ruby及js的我來說,要理解這個概念真的不太容易,在 JavaScript 和 Ruby(或其他非併發語言)中,當一個函數被調用並返回結果後,該函數的執行上下文通常就結束了,goroutine 是如何保持活動狀態的,即使它的主要功能(例如一個函數或任務)已經完成?
我們先來看運作的關係圖
當一個 goroutine 的函數執行完畢,這個 goroutine 就會自動結束,不需要手動進行終止或管理,但有一些服務程序可能會對goroutine有著優雅退出的需求,這邊我們提幾個退出模式
這個模式通常用於那些一旦啟動就不需要再進行交互或管理的 goroutine。它們通常執行一個特定的任務,然後自行結束
一次性任務
go func() {
// 做一些工作...
}()
常駐後台的特定任務
go func() {
for {
// 做一些持續的工作...
}
}()
在傳統的執行緒模型中,一個主執行緒(父執行緒)可以等待它創建的子執行緒完成工作並獲取其結果。這通常是通過 pthread_join 函數來實現的
在 Go 語言中,我們也經常需要主函數(或主 goroutine)等待一個或多個由它創建的 goroutine 完成工作
使用channel
package main
import (
"fmt"
)
func worker(done chan bool) {
fmt.Println("Worker is working...")
// 模擬工作
done <- true // 發送一個信號表示工作已經完成
}
func main() {
done := make(chan bool, 1) // 創建一個通道
go worker(done) // 啟動一個 goroutine
<-done // 等待 goroutine 發送完成的信號
fmt.Println("All goroutines have finished executing")
}
channel 用於同步 goroutine 的執行。當 goroutine 完成工作時,它向 done channel 發送一個信號。主函數通過 <-done 接收這個信號,如果沒有收到信號,主函數會一直阻塞
package main
import (
"fmt"
"time"
)
func worker(resultCh chan int) {
fmt.Println("Worker: 開始工作...")
// 模擬一個長時間的工作
time.Sleep(2 * time.Second)
fmt.Println("Worker: 工作完成!")
// 將結果發送到 channel
resultCh <- 42
}
func main() {
// 創建一個用於接收工作結果的 channel
resultCh := make(chan int)
fmt.Println("Main: 啟動 worker goroutine...")
// 啟動 worker goroutine
go worker(resultCh)
fmt.Println("Main: 等待 worker 完成...")
// 從 channel 接收 worker 的結果
result := <-resultCh
fmt.Println("Main: worker 完成,結果是:", result)
}
函數在 result := <-resultCh 這行代碼處等待,直到 worker goroutine 將結果發送到 resultCh channel,一旦 worker 發送了結果,主函數接收它並繼續執行
當你在 Go 中啟動多個 goroutines 並且你想要在主 goroutine(通常是你的 main 函數)中等待它們全部完成時,你可以使用 sync.WaitGroup。WaitGroup 是一個計數信號量,我們可以使用它來等待一組 goroutines 完成
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 在函數結束時調用 Done 來通知 WaitGroup 這個 worker 已經完成
fmt.Printf("Worker %d: 開始工作...\n", id)
time.Sleep(time.Second) // 模擬一秒的工作時間
fmt.Printf("Worker %d: 工作完成\n", id)
}
func main() {
var wg sync.WaitGroup // 創建一個 WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // 每啟動一個 goroutine 就向 WaitGroup 增加一個計數
go worker(i, &wg) // 啟動一個 worker goroutine
}
// 等待所有的 worker goroutine 完成
wg.Wait()
fmt.Println("所有工作都已完成")
}
管道(Pipeline)模式是一種將一系列處理步驟連接在一起的模式,每個步驟都是一個處理單元,數據在這些處理單元之間通過 channel(通道)傳遞。每個處理單元都是一個 goroutine,所以數據處理步驟是並行執行的
package main
import (
"fmt"
"sync"
)
func producer(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i // 生產數據
}
close(ch) // 關閉 channel
}
func processor(inputCh chan int, outputCh chan int) {
for num := range inputCh { // 處理數據
outputCh <- num * num // 平方
}
close(outputCh) // 關閉 channel
}
func consumer(ch chan int, wg *sync.WaitGroup) {
for num := range ch { // 消費數據
fmt.Println(num)
}
wg.Done() // 通知 WaitGroup 任務完成
}
func main() {
numCh := make(chan int)
squareCh := make(chan int)
var wg sync.WaitGroup
go producer(numCh) // 啟動生產者
go processor(numCh, squareCh) // 啟動處理者
wg.Add(1)
go consumer(squareCh, &wg) // 啟動消費者
wg.Wait() // 等待所有 goroutine 完成
}
以上就是goroutine/channel/併發 的基本用法,用法還有非常多種,在熟悉基本的用法後我們可以再更進一步,直到這一篇為止,我們已經說了非常多基本概念了,明天開始我們要來進入WEB,正式進入後端領域啦~